限流算法
固定窗口
(1)划分时间为多个窗口:固定一个时间周期,如10秒或者30秒
(2)在每个窗口期内,每有一个请求,计数器加一
(3)如果计数器超过了限制数量,则本窗口内所有的请求都被丢弃
(4)下一个时间窗口时,计数器重置
滑动窗口
滑动窗口其实就是对固定窗口做了进一步的细分,将原先的粒度切的更细,比如1分钟的固定窗口切分为60个1秒的滑动窗口。然后统计的时间范围随着时间的推移同步后移。
同时,我们还可以得出一个结论是:如果固定窗口的「固定周期」已经很小了,那么使用滑动窗口的意义也就没有了。举个例子,现在的固定窗口周期已经是1秒了,再切分到毫秒级别能反而得不偿失,会带来巨大的性能和资源损耗。
漏桶
(1)将每个请求视为“水滴”放入漏桶进行存储
(2)漏桶以固定速率漏出水滴(处理请求)
(3)漏桶满了,多余的水滴就丢弃
简单说来就是:如果当前速率小于阈值则直接处理请求,否则不直接处理请求,进入缓冲区,并增加当前水位
漏桶算法多使用队列实现,服务的请求会存到队列中,服务的提供方则按照固定的速率从队列中取出请求并执行,过多的请求则放在队列中排队或直接拒绝。
漏桶算法的缺陷
也很明显,当短时间内有大量的突发请求时,即便此时服务器没有任何负载,每个请求也都得在队列中等待一段时间才能被响应。
令牌桶
(1)令牌以固定速率生成
(2)生成的令牌放入令牌桶中存放,如果令牌桶满了则多余的令牌直接丢弃,当请求到达时,会尝试从令牌桶中取令牌,得到令牌的请求可以执行
(3)如果桶空了,则丢弃取令牌的请求
令牌桶的容量大小理论上就是程序需要支撑的最大并发数。令牌桶算法既能够将所有的请求平均分布到时间区间内,又能接受服务器能够承受范围内的突发请求,因此是目前使用较为广泛的一种限流算法。
对于令牌桶的代码实现,可以直接使用Guava包中的RateLimiter
分布式场景
单节点模式下,使用RateLimiter进行限流一点问题都没有。但线上是分布式系统,布署了多个节点,而且多个节点最终调用的是同一个API/服务商接口。虽然我们对单个节点能做到将QPS限制在N/s,但是多节点条件下,如果每个节点均是N/s,那么到服务商那边的总请求就是节点数乘以N/s,于是限流效果失效。使用该方案对单节点的阈值控制是难以适应分布式环境的。
我们来看一下最简单的流量模型:
用户的请求从网关转发到后台服务,后台服务承接流量,调用缓存获取数据,缓存中的数据和数据库交互。这个模型就像一个漏斗一样,流量自上而下递减。
解决方案一:网关限流
服务网关,作为整个分布式链路中的第一关卡,承接了所有用户的访问请求,所以从这里限流肯定是大头。目前主流的网关层有以软件为代表的Nginx,Spring Cloud中的Gateway和Zuul这类的组件,当然也有硬件的网关限流。
1.Nginx限流:思想就是漏桶算法,即能够强行保证请求实时处理的速度不会超过设置的阈值
解决方案二:服务限流Redis 的 RateLimiter
https://segmentfault.com/a/1190000012947169
@GetMapping("/")
public void index(HttpServletResponse response) throws IOException {
Jedis jedis = jedisPool.getResource();
String token = RedisRateLimiter.acquireTokenFromBucket(jedis, LIMIT, TIMEOUT);
if (token == null) {
response.sendError(500);
}else{
//TODO 你的业务逻辑
}
jedisPool.returnResource(jedis);
}